package com.immediatus.engine.input;

import android.view.MotionEvent;
import com.immediatus.engine.contracts.IGestureDetector;
import com.immediatus.engine.contracts.IGestureListener;
import com.immediatus.util.Vector2;

public class GestureDetector implements IGestureDetector{
    private static final int TRACKED_POINTER_COUNT = 10;

    private final int _tapSquareSize;
	private final long _tapCountInterval;
	private final long _longPressDuration;
    private final VelocityTracker _velocityTracker = new VelocityTracker();

	private long _maxFlingDelay;
	private boolean _inTapSquare;
	private int _tapCount;
	private long _lastTapTime;
	private boolean _longPressFired;
	private boolean _pinching;
	private boolean _panning;
	private int _tapSquareCenterX;
	private int _tapSquareCenterY;
	private long _gestureStartTime;
    private float _initialDistance;

    private boolean [] _pointersTouched = new boolean[TRACKED_POINTER_COUNT];
	private Vector2 _firstPointer = new Vector2();
	private Vector2 _secondPointer = new Vector2();
	private final IGestureListener _gestureListener;

	public GestureDetector (IGestureListener listener_) {
		this(20, 0.4f, 1.5f, 0.15f, listener_);
	}

	public GestureDetector (int halfTapSquareSize_, float tapCountInterval_, float longPressDuration_, float maxFlingDelay_, IGestureListener listener_) {
		this._tapSquareSize = halfTapSquareSize_;
		this._tapCountInterval = (long)(tapCountInterval_ * 1000l);
		this._longPressDuration = (long)(longPressDuration_ * 1000l);
		this._maxFlingDelay = (long)(maxFlingDelay_ * 1000l);
		this._gestureListener = listener_;
	}

    @Override
    public boolean onTouch(final MotionEvent event_){

        for (int i = 0; i < event_.getPointerCount(); i++ ) {

            boolean masked = false;
            switch(event_.getActionMasked()) {
                case MotionEvent.ACTION_POINTER_DOWN:
                case MotionEvent.ACTION_POINTER_UP:
                    masked = true;
                    break;
            }

            if (masked && i != event_.getActionIndex()) { continue; }
            float x = event_.getX(i);
            float y = event_.getY(i);

            int pid = event_.getPointerId(i);
            long eventTime = event_.getEventTime();

            switch(event_.getActionMasked()) {
                case MotionEvent.ACTION_DOWN:
                case MotionEvent.ACTION_POINTER_DOWN:
                    _pointersTouched[pid] = true;
                    return touchDown((int)x, (int)y, pid, eventTime);

                case MotionEvent.ACTION_MOVE:
                case MotionEvent.ACTION_OUTSIDE:
                    return touchDragged((int) x, (int) y, pid, eventTime);

                case MotionEvent.ACTION_UP:
                case MotionEvent.ACTION_CANCEL:
                    for(int j = 0; j < TRACKED_POINTER_COUNT; j++) _pointersTouched[j] = false;

                case MotionEvent.ACTION_POINTER_UP:
                    _pointersTouched[pid] = false;
                    return touchUp((int)x, (int)y, pid, eventTime);
            }
            if (masked) { break; }
        }
        return false;
    }


	public boolean touchDown (final int x_, final int y_, final int pid_, final long eventTime_) {
        if (pid_ > 1) return false;

		if (pid_ == 0){
            this._firstPointer.set(x_, y_);
            this._gestureStartTime = eventTime_;
            this._velocityTracker.start(x_, y_, this._gestureStartTime);

			if (_pointersTouched[1]) { //TODO: refactor this
                this._inTapSquare = false;
                this._pinching = true;
                this._initialDistance = this._firstPointer.dst(this._secondPointer);
			} else {
                this._inTapSquare = true;
                this._pinching = false;
                this._longPressFired = false;
                this._tapSquareCenterX = x_;
                this._tapSquareCenterY = y_;
            }
		} else {
            this._secondPointer.set(x_, y_);
            this._inTapSquare = false;
            this._pinching = true;
            this._initialDistance = this._firstPointer.dst(this._secondPointer);
		}
		return this._gestureListener.touchDown(x_, y_, pid_);
	}

	public boolean touchDragged (final int x_, final int y_, final int pid_, final long eventTime_) {
		if (pid_ > 1) return false;

		if (this._pinching) {
			if (pid_ == 0)
                this._firstPointer.set(x_, y_);
			else
                this._secondPointer.set(x_, y_);

            return this._gestureListener != null && this._gestureListener.zoom(this._initialDistance, this._firstPointer.dst(this._secondPointer));
        }

        this._velocityTracker.update(x_, y_, eventTime_);

		if (!(this._inTapSquare && Math.abs(x_ - this._tapSquareCenterX) < this._tapSquareSize && Math.abs(y_ - this._tapSquareCenterY) < this._tapSquareSize)) {
            this._inTapSquare = false;
		}

		if (!this._inTapSquare) {
            this._inTapSquare = false;
            this._panning = true;
			return this._gestureListener.pan(this._velocityTracker._lastX, this._velocityTracker._lastY, this._velocityTracker._deltaX, this._velocityTracker._deltaY);
		} else {
			if (!_longPressFired && eventTime_ - this._gestureStartTime > this._longPressDuration) {
                this._longPressFired = true;
				return this._gestureListener.longPress(x_, y_);
			}
		}

		return false;
	}

	public boolean touchUp (final int x_, final int y_, final int pid_, final long eventTime_){
		if (pid_ > 1) return false;

        this._panning = false;
		if (this._inTapSquare & !this._longPressFired) {
			if (System.nanoTime() - this._lastTapTime > this._tapCountInterval) this._tapCount = 0;
            this._tapCount++;
            this._lastTapTime = System.nanoTime();
            this._gestureStartTime = 0;
			return this._gestureListener.tap(this._tapSquareCenterX, this._tapSquareCenterY, this._tapCount);
		} else if (this._pinching) {
            this._pinching = false;
            this._panning = true;
            if (pid_ == 0){
                this._velocityTracker.start((int) _secondPointer._x, (int) _secondPointer._y, eventTime_);
            } else{
                this._velocityTracker.start((int) _firstPointer._x, (int) _firstPointer._y, eventTime_);
			}
		} else {
            this._gestureStartTime = 0;
			long time = eventTime_;
			if (time - this._velocityTracker._lastTime < this._maxFlingDelay) {
                this._velocityTracker.update(x_, y_, time);
				return this._gestureListener.fling(this._velocityTracker.getVelocityX(), this._velocityTracker.getVelocityY());
			}
		}
		return false;
	}
	
	public boolean isLongPressed () {
		return isLongPressed(this._longPressDuration);
	}

	public boolean isLongPressed (float duration_) {
        return this._gestureStartTime != 0 && System.nanoTime() - this._gestureStartTime > (long) (duration_ * 1000000000l);
    }

	public boolean isPanning () {
		return this._panning;
	}

    static class VelocityTracker {
        private int _sampleSize = 10;
        private int _lastX;
        private int _lastY;
        private int _deltaX;
        private int _deltaY;
        private long _lastTime;
        private int _numSamples;
        private float[] _meanX = new float[_sampleSize];
        private float[] _meanY = new float[_sampleSize];
        private long[] _meanTime = new long[_sampleSize];

        public void start (int x_, int y_, long timeStamp_) {
            this._lastX = x_;
            this._lastY = y_;
            this._deltaX = 0;
            this._deltaY = 0;
            this._numSamples = 0;
            for (int i = 0; i < this._sampleSize; i++) {
                this._meanX[i] = 0;
                this._meanY[i] = 0;
                this._meanTime[i] = 0;
            }
            this._lastTime = timeStamp_;
        }

        public void update (int x_, int y_, long timeStamp_) {
            long currTime = timeStamp_;
            this._deltaX = (x_ - this._lastX);
            this._deltaY = (y_ - this._lastY);
            this._lastX = x_;
            this._lastY = y_;
            long deltaTime = currTime - this._lastTime;
            this._lastTime = currTime;
            int index = this._numSamples % this._sampleSize;
            this._meanX[index] = this._deltaX;
            this._meanY[index] = this._deltaY;
            this._meanTime[index] = deltaTime;
            this._numSamples++;
        }

        public float getVelocityX () {
            float meanX = getAverage(this._meanX, this._numSamples);
            float meanTime = getAverage(this._meanTime, this._numSamples) / 1000000000.0f;
            if (meanTime == 0) return 0;
            return meanX / meanTime;
        }

        public float getVelocityY () {
            float meanY = getAverage(this._meanY, this._numSamples);
            float meanTime = getAverage(this._meanTime, this._numSamples) / 1000000000.0f;
            if (meanTime == 0) return 0;
            return meanY / meanTime;
        }

        private float getAverage (float[] values_, int numSamples_) {
            numSamples_ = Math.min(this._sampleSize, numSamples_);
            float sum = 0;
            for (int i = 0; i < numSamples_; i++) {
                sum += values_[i];
            }
            return sum / numSamples_;
        }

        private long getAverage (long[] values_, int numSamples_) {
            numSamples_ = Math.min(this._sampleSize, numSamples_);
            long sum = 0;
            for (int i = 0; i < numSamples_; i++) {
                sum += values_[i];
            }
            if (numSamples_ == 0) return 0;
            return sum / numSamples_;
        }

        private float getSum (float[] values_, int numSamples_) {
            numSamples_ = Math.min(this._sampleSize, numSamples_);
            float sum = 0;
            for (int i = 0; i < numSamples_; i++) {
                sum += values_[i];
            }
            if (numSamples_ == 0) return 0;
            return sum;
        }
    }
}
